//@version=5
indicator("Every Hour 1st/Last FVG vTDL ", overlay=true, max_boxes_count=500, max_lines_count=500, dynamic_requests=true)




//    ████████╗██████╗  █████╗ ██████╗ ███████╗███████╗                          
//    ╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝                          
//       ██║   ██████╔╝███████║██║  ██║█████╗  ███████╗                          
//       ██║   ██╔══██╗██╔══██║██║  ██║██╔══╝  ╚════██║                          
//       ██║   ██║  ██║██║  ██║██████╔╝███████╗███████║                          
//       ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚══════╝╚══════╝                          
//                                                                               
//    ██████╗  ██████╗ ███╗   ██╗████████╗    ██╗     ██╗███████╗                
//    ██╔══██╗██╔═══██╗████╗  ██║╚══██╔══╝    ██║     ██║██╔════╝                
//    ██║  ██║██║   ██║██╔██╗ ██║   ██║       ██║     ██║█████╗                  
//    ██║  ██║██║   ██║██║╚██╗██║   ██║       ██║     ██║██╔══╝                  
//    ██████╔╝╚██████╔╝██║ ╚████║   ██║       ███████╗██║███████╗                
//    ╚═════╝  ╚═════╝ ╚═╝  ╚═══╝   ╚═╝       ╚══════╝╚═╝╚══════╝                
//                                                                               
//                                                                                                                                                         
//                                                                               
//                                                                               
//    ███████╗██╗██████╗ ███████╗████████╗                                       
//    ██╔════╝██║██╔══██╗██╔════╝╚══██╔══╝                                       
//    █████╗  ██║██████╔╝███████╗   ██║                                          
//    ██╔══╝  ██║██╔══██╗╚════██║   ██║                                          
//    ██║     ██║██║  ██║███████║   ██║                                          
//    ╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝   ╚═╝                                          
//                                                                               
//    ██████╗ ██████╗ ███████╗███████╗███████╗███╗   ██╗████████╗███████╗██████╗ 
//    ██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝████╗  ██║╚══██╔══╝██╔════╝██╔══██╗
//    ██████╔╝██████╔╝█████╗  ███████╗█████╗  ██╔██╗ ██║   ██║   █████╗  ██║  ██║
//    ██╔═══╝ ██╔══██╗██╔══╝  ╚════██║██╔══╝  ██║╚██╗██║   ██║   ██╔══╝  ██║  ██║
//    ██║     ██║  ██║███████╗███████║███████╗██║ ╚████║   ██║   ███████╗██████╔╝
//    ╚═╝     ╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝╚═╝  ╚═══╝   ╚═╝   ╚══════╝╚═════╝ 
//                                                                               
//    ███████╗██╗   ██╗ ██████╗                                                  
//    ██╔════╝██║   ██║██╔════╝                                                  
//    █████╗  ██║   ██║██║  ███╗                                                 
//    ██╔══╝  ╚██╗ ██╔╝██║   ██║                                                 
//    ██║      ╚████╔╝ ╚██████╔╝                                                 
//    ╚═╝       ╚═══╝   ╚═════╝                                                  
//                                                 



// ============================================================================
// v2 OPTIMIZATION SUMMARY:
// 1. User-Defined Types (UDT) - Single array instead of 15+ parallel arrays
// 2. Early exits for filled boxes in processing loop
// 3. Cached session boundary calculations (once per bar, not per box)
// 4. Consolidated bull/bear box creation into single function
// 5. Reduced redundant array.get() calls via UDT field access
// 6. Conditional midpoint processing (skipped when disabled)
// ============================================================================

// ============================================================================
// INPUT GROUPS
// ============================================================================

detectionType = input.string("FVG", "Detection Type",
     options=["FVG", "Suspension FVG Blocks"],
     group="Detection Settings",
     tooltip="FVG: Just a FVG\nSuspension FVG Blocks: Requires VI on BOTH sides of FVG")

ltfResolution = input.string("1", "Lower Timeframe",
     options=["15S", "1", "5"],
     group="Detection Settings",
     tooltip="Base timeframe for FVG detection")

minFvgTickSize = input.int(4, "Min FVG Size (Ticks)", minval=1,
     group="Detection Settings")

hourlyDisplayMode = input.string("First Only", "Hourly Display Mode",
     options=["First Only", "Show Both"],
     group="Display Settings",
     tooltip="First Only: Show whichever appears FIRST per hour (bull OR bear, not both)\nShow Both: Show FIRST bullish AND FIRST bearish independently (thats that sauce bro)")

showFirstFvg = input.bool(true, "Show First FVG",
     group="Display Settings",
     tooltip="Display the FIRST FVG of each hour")

showLastFvg = input.bool(false, "Show Last FVG",
     group="Display Settings",
     tooltip="Toggle ON to also display the LAST FVG that forms in each hour (in addition to First FVG)\nLast FVGs use different colors. Extend them into the next hour if you would like. I do. ")

lastFvgDisplayMode = input.string("Last Only", "Last FVG Display Mode",
     options=["Last Only", "Show Both"],
     group="Display Settings",
     tooltip="Last Only: Show whichever appears LAST per hour (bull OR bear, not both)\nShow Both: Show LAST bullish AND LAST bearish independently (no really its that sauce bro)")

extendLastFvgIntoNextHour = input.bool(false, "Extend Last FVG Into Next Hour",
     group="Display Settings")

bullColor = input.color(color.new(#2962FF, 75), "First Bullish FVG Color",
     group="Display Settings")

bearColor = input.color(color.new(#FF1744, 75), "First Bearish FVG Color",
     group="Display Settings")

lastBullColor = input.color(color.new(#00E676, 75), "Last Bullish FVG Color",
     group="Display Settings")

lastBearColor = input.color(color.new(#FF9800, 75), "Last Bearish FVG Color",
     group="Display Settings")

showMidpoints = input.bool(true, "Show Midpoint Lines",
     group="Display Settings")

midpointColor = input.color(color.new(color.black, 0), "Midpoint Line Color",
     group="Display Settings")

labelTextColor = input.color(color.black, "Label Text Color",
     group="Display Settings")

showUnfilledGaps = input.bool(true, "Show Unfilled Breakaway Gaps",
     group="Display Settings")

showOnlyToday = input.bool(false, "Show Only Today's Boxes",
     group="Display Settings")

gapFillMode = input.string("Touch Box", "Gap Fill Detection Mode",
     options=["Touch Box", "Touch Midpoint", "Fill Completely"],
     group="Display Settings")

bullFilledColor = input.color(color.new(#2962FF, 60), "First Bullish Filled Portion Color",
     group="Display Settings",
     tooltip="Color for filled portion of bullish gaps in 'Fill Completely' mode")

bearFilledColor = input.color(color.new(#FF1744, 60), "First Bearish Filled Portion Color",
     group="Display Settings",
     tooltip="Color for filled portion of bearish gaps in 'Fill Completely' mode")

lastBullFilledColor = input.color(color.new(#00E676, 60), "Last Bullish Filled Portion Color",
     group="Display Settings",
     tooltip="Color for filled portion of last bullish gaps in 'Fill Completely' mode")

lastBearFilledColor = input.color(color.new(#FF9800, 60), "Last Bearish Filled Portion Color",
     group="Display Settings",
     tooltip="Color for filled portion of last bearish gaps in 'Fill Completely' mode")

midpointStyle = input.string("Solid", "Midpoint Line Style",
     options=["Solid", "Dotted", "Dashed"],
     group="Display Settings")

// ============================================================================
// CONSTANTS
// ============================================================================

string sessionTimezone = "America/New_York"

midpointStyleConverted =
     midpointStyle == "Solid" ? line.style_solid :
     midpointStyle == "Dotted" ? line.style_dotted :
     line.style_dashed

// ============================================================================
// USER-DEFINED TYPE - MAJOR OPTIMIZATION
// Replaces 15+ parallel arrays with single structured type
// ============================================================================

type FvgBox
    box     mainBox
    line    midLine
    label   boxLabel
    box     overlayBox
    float   top
    float   bot
    int     formationTime
    int     confirmTime
    int     hourEnd
    bool    isFilled
    bool    wasHit
    bool    isProcessed
    bool    isLast
    bool    isBull
    float   fillProgress  // lowestPrice for bull, highestPrice for bear

// ============================================================================
// STATE VARIABLES
// ============================================================================

var bool currentHourBullFound = false
var bool currentHourBearFound = false

// Last FVG candidate tracking
var bool hasLastBull = false
var bool hasLastBear = false
var float lastBullTop = na
var float lastBullBot = na
var int lastBullFormationTime = na
var int lastBullConfirmTime = na
var bool lastBullWasHit = false
var float lastBearTop = na
var float lastBearBot = na
var int lastBearFormationTime = na
var int lastBearConfirmTime = na
var bool lastBearWasHit = false

var int lastHour = na

// OPTIMIZED: Single array for all FVG boxes
var array<FvgBox> allBoxes = array.new<FvgBox>()

// For HTF: Store last 2 LTF candles
var float prev1LTFCandleH = na
var float prev1LTFCandleL = na
var float prev2LTFCandleH = na
var float prev2LTFCandleL = na
var int prev1LTFCandleT = na
var int prev2LTFCandleT = na

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

getHourEndTime(int t, string tz) =>
    int hourStart = timestamp(tz, year(t, tz), month(t, tz), dayofmonth(t, tz), hour(t, tz), 0, 0)
    hourStart + 3600000

detectFvg(string detType, float h0, float h1, float h2, float l0, float l1, float l2,
          float c0, float c1, float c2, float o0, float o1, float o2, float minGap) =>
    bool isBull = false
    bool isBear = false
    float bullTop = 0.0
    float bullBot = 0.0
    float bearTop = 0.0
    float bearBot = 0.0

    if detType == "FVG"
        isBull := h2 < l0 and (l0 - h2) >= minGap
        bullTop := l0
        bullBot := h2

        if isBull
            float body2Top = math.max(c2, o2)
            float body1Bot = math.min(c1, o1)
            float body1Top = math.max(c1, o1)
            float body0Bot = math.min(c0, o0)

            if body2Top < body1Bot
                bullBot := body2Top
            if body1Top < body0Bot
                bullTop := body0Bot

        isBear := l2 > h0 and (l2 - h0) >= minGap
        bearTop := l2
        bearBot := h0

        if isBear
            float body2Bot = math.min(c2, o2)
            float body1Top = math.max(c1, o1)
            float body1Bot = math.min(c1, o1)
            float body0Top = math.max(c0, o0)

            if body2Bot > body1Top
                bearTop := body2Bot
            if body1Bot > body0Top
                bearBot := body0Top

    else if detType == "Suspension FVG Blocks"
        isBull := h2 < l0 and (l0 - h2) >= minGap
        bullTop := l0
        bullBot := h2

        if isBull
            float body2Top = math.max(c2, o2)
            float body1Bot = math.min(c1, o1)
            float body1Top = math.max(c1, o1)
            float body0Bot = math.min(c0, o0)

            bool outsidePrint_2_1 = body2Top < body1Bot
            bool outsidePrint_1_0 = body1Top < body0Bot

            if outsidePrint_2_1 and outsidePrint_1_0
                bullBot := body2Top
                bullTop := body0Bot
            else
                isBull := false

        isBear := l2 > h0 and (l2 - h0) >= minGap
        bearTop := l2
        bearBot := h0

        if isBear
            float body2Bot = math.min(c2, o2)
            float body1Top = math.max(c1, o1)
            float body1Bot = math.min(c1, o1)
            float body0Top = math.max(c0, o0)

            bool outsidePrint_2_1 = body2Bot > body1Top
            bool outsidePrint_1_0 = body1Bot > body0Top

            if outsidePrint_2_1 and outsidePrint_1_0
                bearTop := body2Bot
                bearBot := body0Top
            else
                isBear := false

    [isBull, isBear, bullTop, bullBot, bearTop, bearBot]

// OPTIMIZED: Consolidated box creation function
createFvgBox(float boxTop, float boxBot, int formTime, int confTime, int hourEnd, bool isBull, bool isLast) =>
    color boxColor = isBull ? (isLast ? lastBullColor : bullColor) : (isLast ? lastBearColor : bearColor)
    
    box newBox = box.new(left = formTime, top = boxTop, right = hourEnd, bottom = boxBot, 
         bgcolor = boxColor, border_color = boxColor, border_width = 1, xloc = xloc.bar_time)
    
    line newMidLine = na
    if showMidpoints
        float mid = (boxTop + boxBot) / 2.0
        newMidLine := line.new(formTime, mid, hourEnd, mid, color=midpointColor, 
             style=midpointStyleConverted, width=1, xloc=xloc.bar_time)
    
    FvgBox fvg = FvgBox.new(
         mainBox = newBox,
         midLine = newMidLine,
         boxLabel = na,
         overlayBox = na,
         top = boxTop,
         bot = boxBot,
         formationTime = formTime,
         confirmTime = confTime,
         hourEnd = hourEnd,
         isFilled = false,
         wasHit = false,
         isProcessed = false,
         isLast = isLast,
         isBull = isBull,
         fillProgress = isBull ? boxTop : boxBot)
    
    fvg

// ============================================================================
// HOUR CHANGE DETECTION
// ============================================================================

int currentHour = hour(time, sessionTimezone)
bool isNewHour = na(lastHour) or currentHour != lastHour

int currentDayOfWeek = dayofweek(time, sessionTimezone)
bool isWeekendClosed = currentDayOfWeek == dayofweek.saturday or (currentDayOfWeek == dayofweek.sunday and currentHour < 18)

if isNewHour
    // Create last FVG boxes from previous hour
    bool createLastBull = false
    bool createLastBear = false

    if showLastFvg
        if lastFvgDisplayMode == "Show Both"
            createLastBull := hasLastBull and not na(lastBullFormationTime)
            createLastBear := hasLastBear and not na(lastBearFormationTime)
        else
            if hasLastBull and hasLastBear and not na(lastBullFormationTime) and not na(lastBearFormationTime)
                if lastBullFormationTime > lastBearFormationTime
                    createLastBull := true
                else
                    createLastBear := true
            else if hasLastBull and not na(lastBullFormationTime)
                createLastBull := true
            else if hasLastBear and not na(lastBearFormationTime)
                createLastBear := true

    if createLastBull
        int lastBullHour = hour(lastBullFormationTime, sessionTimezone)
        if lastBullHour != 16 and lastBullHour != 17
            int baseHourEnd = getHourEndTime(lastBullFormationTime, sessionTimezone)
            int boxRightEdge = extendLastFvgIntoNextHour ? baseHourEnd + 3600000 : baseHourEnd
            
            FvgBox newFvg = createFvgBox(lastBullTop, lastBullBot, lastBullFormationTime, 
                 lastBullConfirmTime, boxRightEdge, true, true)
            newFvg.wasHit := lastBullWasHit
            newFvg.isProcessed := true
            array.push(allBoxes, newFvg)

    if createLastBear
        int lastBearHour = hour(lastBearFormationTime, sessionTimezone)
        if lastBearHour != 16 and lastBearHour != 17
            int baseHourEnd = getHourEndTime(lastBearFormationTime, sessionTimezone)
            int boxRightEdge = extendLastFvgIntoNextHour ? baseHourEnd + 3600000 : baseHourEnd
            
            FvgBox newFvg = createFvgBox(lastBearTop, lastBearBot, lastBearFormationTime, 
                 lastBearConfirmTime, boxRightEdge, false, true)
            newFvg.wasHit := lastBearWasHit
            newFvg.isProcessed := true
            array.push(allBoxes, newFvg)

    // Reset tracking
    hasLastBull := false
    hasLastBear := false
    lastBullTop := na
    lastBullBot := na
    lastBullFormationTime := na
    lastBullConfirmTime := na
    lastBullWasHit := false
    lastBearTop := na
    lastBearBot := na
    lastBearFormationTime := na
    lastBearConfirmTime := na
    lastBearWasHit := false
    lastHour := currentHour
    currentHourBullFound := false
    currentHourBearFound := false

// ============================================================================
// TIMEFRAME DETECTION
// ============================================================================

float minTickValue = minFvgTickSize * syminfo.mintick
bool isChartBaseTF = timeframe.period == ltfResolution

bool isBullishFvg = false
bool isBearishFvg = false
float fvgBullTop = 0.0
float fvgBullBot = 0.0
float fvgBearTop = 0.0
float fvgBearBot = 0.0
int fvgFormationTime = 0

// ============================================================================
// BRANCH 1: CHART = BASE TIMEFRAME
// ============================================================================

if isChartBaseTF
    [isBull, isBear, bTop, bBot, rTop, rBot] = detectFvg(detectionType, high[0], high[1], high[2], 
         low[0], low[1], low[2], close[0], close[1], close[2], open[0], open[1], open[2], minTickValue)

    int oldestHour = hour(time[2], sessionTimezone)
    int middleHour = hour(time[1], sessionTimezone)
    int confirmHour = hour(time[0], sessionTimezone)

    bool validHours = oldestHour != 16 and oldestHour != 17 and middleHour != 16 and middleHour != 17 and confirmHour != 16 and confirmHour != 17

    if isBull and middleHour == currentHour and validHours
        if hourlyDisplayMode == "Show Both" or not isBearishFvg
            isBullishFvg := isBull
            fvgBullTop := bTop
            fvgBullBot := bBot
            fvgFormationTime := time[2]

        hasLastBull := true
        lastBullTop := bTop
        lastBullBot := bBot
        lastBullFormationTime := time[2]
        lastBullConfirmTime := time
        lastBullWasHit := false

    if isBear and middleHour == currentHour and validHours
        if hourlyDisplayMode == "Show Both" or not isBullishFvg
            isBearishFvg := isBear
            fvgBearTop := rTop
            fvgBearBot := rBot
            fvgFormationTime := time[2]

        hasLastBear := true
        lastBearTop := rTop
        lastBearBot := rBot
        lastBearFormationTime := time[2]
        lastBearConfirmTime := time
        lastBearWasHit := false

// ============================================================================
// BRANCH 2: CHART > BASE TIMEFRAME (request.security_lower_tf)
// ============================================================================

else
    [ltfTime, ltfHigh, ltfLow, ltfClose, ltfOpen] = request.security_lower_tf(syminfo.tickerid, ltfResolution, [time, high, low, close, open])

    // Reusable arrays for combining LTF data
    var int[] combinedTime = array.new_int()
    var float[] combinedHigh = array.new_float()
    var float[] combinedLow = array.new_float()
    var float[] combinedClose = array.new_float()
    var float[] combinedOpen = array.new_float()

    array.clear(combinedTime)
    array.clear(combinedHigh)
    array.clear(combinedLow)
    array.clear(combinedClose)
    array.clear(combinedOpen)

    if not na(prev2LTFCandleT)
        array.push(combinedTime, prev2LTFCandleT)
        array.push(combinedHigh, prev2LTFCandleH)
        array.push(combinedLow, prev2LTFCandleL)
        array.push(combinedClose, na)
        array.push(combinedOpen, na)

    if not na(prev1LTFCandleT)
        array.push(combinedTime, prev1LTFCandleT)
        array.push(combinedHigh, prev1LTFCandleH)
        array.push(combinedLow, prev1LTFCandleL)
        array.push(combinedClose, na)
        array.push(combinedOpen, na)

    int currentBarStartIdx = array.size(combinedTime)

    if not na(ltfTime) and array.size(ltfTime) > 0
        for i = 0 to array.size(ltfTime) - 1
            array.push(combinedTime, array.get(ltfTime, i))
            array.push(combinedHigh, array.get(ltfHigh, i))
            array.push(combinedLow, array.get(ltfLow, i))
            array.push(combinedClose, array.get(ltfClose, i))
            array.push(combinedOpen, array.get(ltfOpen, i))

    int fetchedSize = not na(ltfTime) ? array.size(ltfTime) : 0
    if fetchedSize >= 2
        prev1LTFCandleT := array.get(ltfTime, fetchedSize - 1)
        prev1LTFCandleH := array.get(ltfHigh, fetchedSize - 1)
        prev1LTFCandleL := array.get(ltfLow, fetchedSize - 1)
        prev2LTFCandleT := array.get(ltfTime, fetchedSize - 2)
        prev2LTFCandleH := array.get(ltfHigh, fetchedSize - 2)
        prev2LTFCandleL := array.get(ltfLow, fetchedSize - 2)
    else if fetchedSize == 1
        prev1LTFCandleT := array.get(ltfTime, fetchedSize - 1)
        prev1LTFCandleH := array.get(ltfHigh, fetchedSize - 1)
        prev1LTFCandleL := array.get(ltfLow, fetchedSize - 1)

    int combinedSize = array.size(combinedTime)

    if combinedSize >= 3
        for i = 2 to combinedSize - 1
            if i >= currentBarStartIdx
                int oldestCandleTime = array.get(combinedTime, i - 2)
                int middleCandleTime = array.get(combinedTime, i - 1)
                int confirmTimeVal = array.get(combinedTime, i)

                int oldestHour = hour(oldestCandleTime, sessionTimezone)
                int middleHour = hour(middleCandleTime, sessionTimezone)
                int confirmHour = hour(confirmTimeVal, sessionTimezone)

                bool validHours = oldestHour != 16 and oldestHour != 17 and middleHour != 16 and middleHour != 17 and confirmHour != 16 and confirmHour != 17

                if middleHour == currentHour and currentHour != 16 and currentHour != 17 and validHours
                    float h0 = array.get(combinedHigh, i)
                    float h1 = array.get(combinedHigh, i - 1)
                    float h2 = array.get(combinedHigh, i - 2)
                    float l0 = array.get(combinedLow, i)
                    float l1 = array.get(combinedLow, i - 1)
                    float l2 = array.get(combinedLow, i - 2)
                    float c0 = array.get(combinedClose, i)
                    float c1 = array.get(combinedClose, i - 1)
                    float c2 = array.get(combinedClose, i - 2)
                    float o0 = array.get(combinedOpen, i)
                    float o1 = array.get(combinedOpen, i - 1)
                    float o2 = array.get(combinedOpen, i - 2)

                    [isBull, isBear, bTop, bBot, rTop, rBot] = detectFvg(detectionType, h0, h1, h2, l0, l1, l2, c0, c1, c2, o0, o1, o2, minTickValue)

                    if isBull and not isBullishFvg
                        if hourlyDisplayMode == "Show Both" or not isBearishFvg
                            isBullishFvg := true
                            fvgBullTop := bTop
                            fvgBullBot := bBot
                            fvgFormationTime := middleCandleTime

                    if isBull
                        hasLastBull := true
                        lastBullTop := bTop
                        lastBullBot := bBot
                        lastBullFormationTime := middleCandleTime
                        lastBullConfirmTime := time
                        lastBullWasHit := false

                    if isBear and not isBearishFvg
                        if hourlyDisplayMode == "Show Both" or not isBullishFvg
                            isBearishFvg := true
                            fvgBearTop := rTop
                            fvgBearBot := rBot
                            fvgFormationTime := middleCandleTime

                    if isBear
                        hasLastBear := true
                        lastBearTop := rTop
                        lastBearBot := rBot
                        lastBearFormationTime := middleCandleTime
                        lastBearConfirmTime := time
                        lastBearWasHit := false

// ============================================================================
// LAST FVG MITIGATION TRACKING
// ============================================================================

if hasLastBull and not lastBullWasHit and not na(lastBullConfirmTime) and time > lastBullConfirmTime
    if (low < lastBullTop) and (high > lastBullBot)
        lastBullWasHit := true

if hasLastBear and not lastBearWasHit and not na(lastBearConfirmTime) and time > lastBearConfirmTime
    if (low < lastBearTop) and (high > lastBearBot)
        lastBearWasHit := true

// ============================================================================
// DISPLAY PERMISSION
// ============================================================================

bool plotPermissionBull = true
bool plotPermissionBear = true

if hourlyDisplayMode == "First Only"
    if currentHourBullFound or currentHourBearFound
        plotPermissionBull := false
        plotPermissionBear := false
else
    if currentHourBullFound
        plotPermissionBull := false
    if currentHourBearFound
        plotPermissionBear := false

// ============================================================================
// BOX CREATION
// ============================================================================

int formationHour = hour(fvgFormationTime, sessionTimezone)
bool validFormationHour = formationHour != 16 and formationHour != 17

if showFirstFvg and isBullishFvg and plotPermissionBull and not currentHourBullFound and currentHour != 16 and currentHour != 17 and validFormationHour and not isWeekendClosed
    int boxRightEdge = getHourEndTime(time, sessionTimezone)
    FvgBox newFvg = createFvgBox(fvgBullTop, fvgBullBot, fvgFormationTime, time, boxRightEdge, true, false)
    array.push(allBoxes, newFvg)
    
    currentHourBullFound := true
    if hourlyDisplayMode == "First Only"
        currentHourBearFound := true

if showFirstFvg and isBearishFvg and plotPermissionBear and not currentHourBearFound and currentHour != 16 and currentHour != 17 and validFormationHour and not isWeekendClosed
    int boxRightEdge = getHourEndTime(time, sessionTimezone)
    FvgBox newFvg = createFvgBox(fvgBearTop, fvgBearBot, fvgFormationTime, time, boxRightEdge, false, false)
    array.push(allBoxes, newFvg)
    
    currentHourBearFound := true
    if hourlyDisplayMode == "First Only"
        currentHourBullFound := true

// ============================================================================
// BOX EXTENSION & FILL DETECTION - OPTIMIZED
// ============================================================================

// OPTIMIZATION: Calculate session boundaries ONCE per bar (not per box)
var int sessionStartTime = 0
var int sessionEndTime = 0

if showOnlyToday
    int currentYear = year(time, sessionTimezone)
    int currentMonth = month(time, sessionTimezone)
    int currentDay = dayofmonth(time, sessionTimezone)
    int currentHourCheck = hour(time, sessionTimezone)

    int sessionStartYear = currentYear
    int sessionStartMonth = currentMonth
    int sessionStartDay = currentDay

    if currentHourCheck < 18
        int yesterdayTimestamp = time - 86400000
        sessionStartYear := year(yesterdayTimestamp, sessionTimezone)
        sessionStartMonth := month(yesterdayTimestamp, sessionTimezone)
        sessionStartDay := dayofmonth(yesterdayTimestamp, sessionTimezone)

    sessionStartTime := timestamp(sessionTimezone, sessionStartYear, sessionStartMonth, sessionStartDay, 18, 0, 0)
    
    if currentHourCheck < 18
        sessionEndTime := timestamp(sessionTimezone, currentYear, currentMonth, currentDay, 16, 0, 0)
    else
        int tomorrowTimestamp = time + 86400000
        sessionEndTime := timestamp(sessionTimezone, year(tomorrowTimestamp, sessionTimezone), 
             month(tomorrowTimestamp, sessionTimezone), dayofmonth(tomorrowTimestamp, sessionTimezone), 16, 0, 0)

// Process all boxes - SINGLE LOOP with early exits
int boxCount = array.size(allBoxes)
if boxCount > 0
    for i = boxCount - 1 to 0
        FvgBox fvg = array.get(allBoxes, i)
        
        // OPTIMIZATION: Early exit for filled boxes
        if fvg.isFilled
            continue
        
        // Show Only Today filter - delete boxes outside session
        if showOnlyToday
            int boxRightEdge = box.get_right(fvg.mainBox)
            bool isAtHourBoundary = boxRightEdge <= fvg.hourEnd
            
            if isAtHourBoundary and (fvg.formationTime < sessionStartTime or fvg.formationTime >= sessionEndTime)
                box.delete(fvg.mainBox)
                if not na(fvg.midLine)
                    line.delete(fvg.midLine)
                if not na(fvg.boxLabel)
                    label.delete(fvg.boxLabel)
                if not na(fvg.overlayBox)
                    box.delete(fvg.overlayBox)
                array.remove(allBoxes, i)
                continue
        
        // Process unfilled gap extensions
        if showUnfilledGaps
            // Cache frequently accessed values
            float boxTop = fvg.top
            float boxBot = fvg.bot
            float boxMidpoint = (boxTop + boxBot) / 2.0
            
            // PHASE 1: During formation hour - check if hit
            if time <= fvg.hourEnd and time > fvg.confirmTime
                if not fvg.wasHit
                    if (low < boxTop) and (high > boxBot)
                        fvg.wasHit := true
            
            // PHASE 2 & 3: After formation hour - extend if not hit
            else if time > fvg.hourEnd
                if not fvg.isProcessed
                    fvg.isProcessed := true
                
                if not fvg.wasHit
                    // Check fill condition based on mode
                    bool priceEntered = false
                    
                    if fvg.isBull
                        if gapFillMode == "Touch Box"
                            priceEntered := low <= boxTop and high >= boxBot
                        else if gapFillMode == "Touch Midpoint"
                            priceEntered := low <= boxMidpoint
                        else  // Fill Completely
                            priceEntered := low <= boxBot
                            if low < boxTop and low < fvg.fillProgress
                                fvg.fillProgress := low
                    else  // Bear
                        if gapFillMode == "Touch Box"
                            priceEntered := low <= boxTop and high >= boxBot
                        else if gapFillMode == "Touch Midpoint"
                            priceEntered := high >= boxMidpoint
                        else  // Fill Completely
                            priceEntered := high >= boxTop
                            if high > boxBot and high > fvg.fillProgress
                                fvg.fillProgress := high
                    
                    if priceEntered
                        // Box is filled - mark and clean up label
                        fvg.isFilled := true
                        if not na(fvg.boxLabel)
                            label.delete(fvg.boxLabel)
                            fvg.boxLabel := na
                    else
                        // Extend box to current time
                        box.set_right(fvg.mainBox, time)
                        
                        // Extend midpoint line if enabled
                        if showMidpoints and not na(fvg.midLine)
                            line.set_x2(fvg.midLine, time)
                        
                        // Update/create label
                        int hourValue = hour(fvg.formationTime, sessionTimezone)
                        string ampm = hourValue >= 12 ? "pm" : "am"
                        int hour12 = hourValue > 12 ? hourValue - 12 : (hourValue == 0 ? 12 : hourValue)
                        
                        string labelText = str.format("{0} {1}/{2}",
                             str.tostring(hour12) + ampm,
                             str.tostring(month(fvg.formationTime, sessionTimezone)),
                             str.tostring(dayofmonth(fvg.formationTime, sessionTimezone)))
                        
                        float labelY = boxMidpoint
                        color lblBgColor = fvg.isBull ? (fvg.isLast ? lastBullColor : bullColor) : (fvg.isLast ? lastBearColor : bearColor)
                        
                        if na(fvg.boxLabel)
                            fvg.boxLabel := label.new(time, labelY, labelText,
                                 xloc=xloc.bar_time, yloc=yloc.price,
                                 style=label.style_label_left,
                                 color=color.new(lblBgColor, 100), textcolor=labelTextColor,
                                 size=size.small)
                        else
                            label.set_xy(fvg.boxLabel, time, labelY)
                            label.set_text(fvg.boxLabel, labelText)
                        
                        // Visual overlay for "Fill Completely" mode
                        if gapFillMode == "Fill Completely"
                            if fvg.isBull
                                if fvg.fillProgress < boxTop
                                    float overlayBottom = math.max(fvg.fillProgress, boxBot)
                                    color filledColor = fvg.isLast ? lastBullFilledColor : bullFilledColor
                                    
                                    if na(fvg.overlayBox)
                                        fvg.overlayBox := box.new(left = fvg.formationTime, top = boxTop,
                                             right = time, bottom = overlayBottom,
                                             bgcolor = filledColor, border_color = filledColor,
                                             border_width = 1, xloc = xloc.bar_time)
                                    else
                                        box.set_right(fvg.overlayBox, time)
                                        box.set_bottom(fvg.overlayBox, overlayBottom)
                            else  // Bear
                                if fvg.fillProgress > boxBot
                                    float overlayTop = math.min(fvg.fillProgress, boxTop)
                                    color filledColor = fvg.isLast ? lastBearFilledColor : bearFilledColor
                                    
                                    if na(fvg.overlayBox)
                                        fvg.overlayBox := box.new(left = fvg.formationTime, top = overlayTop,
                                             right = time, bottom = boxBot,
                                             bgcolor = filledColor, border_color = filledColor,
                                             border_width = 1, xloc = xloc.bar_time)
                                    else
                                        box.set_right(fvg.overlayBox, time)
                                        box.set_top(fvg.overlayBox, overlayTop)
